package propertymgmt.entities;

import java.io.Serializable;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Version;

import propertymgmt.constants.Constants.PaymentType;
import propertymgmt.interfaces.Reportable;

/**
 * Lease.java
 * 
 * Represents a Lease.  This class is an Entity, meaning
 * it will be a persistant object within the database.
 * 
 * @author Brian
 *
 */
@SuppressWarnings("serial")
@Entity(name = "LEASE")

public class Lease implements Serializable, Reportable{

	@Id 
	@Column(name = "LEASE_ID", nullable = false)
	@GeneratedValue(strategy = GenerationType.AUTO)
	private long leaseId;
	
	@Column(name = "LEASE_START_DATE", nullable = false)
	private Date leaseStartDate = null;
	
	@Column(name = "LEASE_END_DATE", nullable = false)
	private Date leaseEndDate = null;
	
	@Column(name = "DEPOSIT_AMOUNT", precision = 2, nullable = false)
	private double depositAmount = 0.0;
	
	@Column(name = "RENT_AMOUNT", precision = 2, nullable = false)
	private double rentAmount = 0.0;
	
	@Column(name = "RENT_DUE_DATE")
	private int rentDueDate = 0;
	
	@Column(name = "RENT_LATE_DATE")
	private int rentLateDate = 0;
	
	@Column(name = "LATE_RENT_FEE")
	private double lateRentFee = 0.0;
	
	@Column(name = "LATE_RENT_FEE_PER_DAY")
	private double lateRentFeePerDay = 0.0;
	
	@Column(name = "PETS_ALLOWED")
	private boolean petsAllowed = false;
	
	@Column(name = "PET_DEPOSIT_AMOUNT")
	private double petDepositAmount = 0.0;
	
	@Column(name = "PET_DEPOSIT_REFUNDABLE")
	private boolean petDepositRefundable = false;
	
	@Version
	@Column(name = "LAST_UPDATED_TIME")
	private Date updatedTime;
	
	@Column(name = "TENANTS")
	@OneToMany(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
	private List<Tenant> tenants = null;
	
	@Column(name = "PAYMENTS")
	@OneToMany(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
	private List<Payment> payments = null;
	
	@Column(name = "PROPERTY")
	@OneToOne(fetch=FetchType.EAGER)
	private Property property = null;
	
	/**
	 * Default constructor
	 */
	public Lease(){
		
	}
	
	/**
	 * Constructor
	 * 
	 * @param property Property this Lease belongs to
	 * @param leaseStartDate Date of Lease start
	 * @param leaseEndDate Date of lease end
	 * @param depositAmount deposit amount
	 * @param rentAmount rent amount
	 * @param rentDueDate day of month rent is due
	 * @param rentLateDate day of month rent is late
	 * @param lateRentFee flat fee for being late on rent payment
	 * @param lateRentFeePerDay per day fee for being late on rent payment
	 * @param tenants List of Tenants associated with this Lease
	 * @param petsAllowed true if pets are allowed during this Lease
	 * @param petDepositFee if <code>petsAllowed</code> is true, the deposit required for pets
	 * @param petDepositRefundable if <code>petsAllowed</code> is true, true if the pet deposit is refundable
	 */
	public Lease(Property property, Date leaseStartDate, Date leaseEndDate,
			double depositAmount, double rentAmount, int rentDueDate, int rentLateDate,
			double lateRentFee, double lateRentFeePerDay, List<Tenant> tenants,
			boolean petsAllowed, double petDepositFee, boolean petDepositRefundable) {
		
		this.property = property;
		this.leaseStartDate = leaseStartDate;
		this.leaseEndDate = leaseEndDate;
		this.depositAmount = depositAmount;
		this.rentAmount = rentAmount;
		this.rentDueDate = rentDueDate;
		this.rentLateDate = rentLateDate;
		this.lateRentFee = lateRentFee;
		this.lateRentFeePerDay = lateRentFeePerDay;
		this.tenants = tenants;
		this.petsAllowed = petsAllowed;
		this.petDepositAmount = petDepositFee;
		this.petDepositRefundable = petDepositRefundable;
		this.payments = new ArrayList<Payment>();
	}
	
	/**
	 * Returns a formatted String describing this Lease
	 * 
	 * @return String
	 */
	public String getFormattedString(){
    	SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");
    	
    	String startDate = sdf.format(getLeaseStartDate()).toString();
		String endDate = sdf.format(getLeaseEndDate()).toString();
		
		return startDate + " - " + endDate;		
	}

	/**
	 * Returns the Property associated with this Lease
	 * 
	 * @return Property
	 */
	public Property getProperty(){
		return property;
	}
	
	/**
	 * @return the leaseStartDate
	 */
	public Date getLeaseStartDate() {
		return leaseStartDate;
	}

	/**
	 * @param leaseStartDate the leaseStartDate to set
	 */
	public void setLeaseStartDate(Date leaseStartDate) {
		this.leaseStartDate = leaseStartDate;
	}

	/**
	 * @return the leaseEndDate
	 */
	public Date getLeaseEndDate() {
		return leaseEndDate;
	}

	/**
	 * @param leaseEndDate the leaseEndDate to set
	 */
	public void setLeaseEndDate(Date leaseEndDate) {
		this.leaseEndDate = leaseEndDate;
	}

	/**
	 * @return the tenants
	 */
	public List<Tenant> getTenants() {
		return tenants;
	}

	/**
	 * @param tenants the tenants to set
	 */
	public void setTenants(List<Tenant> tenants) {
		this.tenants = tenants;
	}
	
	/**
	 * @return the depositAmount
	 */
	public double getDepositAmount() {
		return depositAmount;
	}

	/**
	 * @param depositAmount the depositAmount to set
	 */
	public void setDepositAmount(double depositAmount) {
		this.depositAmount = depositAmount;
	}

	/**
	 * @return the rentAmount
	 */
	public double getRentAmount() {
		return rentAmount;
	}
	
	/**
	 * @return id
	 */
	public long getId() {
		return leaseId;
	}

	/**
	 * @param rentAmount the rentAmount to set
	 */
	public void setRentAmount(double rentAmount) {
		this.rentAmount = rentAmount;
	}

	/**
	 * @return the rentDueDate
	 */
	public int getRentDueDate() {
		return rentDueDate;
	}

	/**
	 * @param rentDueDate the rentDueDate to set
	 */
	public void setRentDueDate(int rentDueDate) {
		this.rentDueDate = rentDueDate;
	}

	/**
	 * @return the rentLateDate
	 */
	public int getRentLateDate() {
		return rentLateDate;
	}

	/**
	 * @param rentLateDate the rentLateDate to set
	 */
	public void setRentLateDate(int rentLateDate) {
		this.rentLateDate = rentLateDate;
	}

	/**
	 * @return the lateRentFee
	 */
	public double getLateRentFee() {
		return lateRentFee;
	}

	/**
	 * @param lateRentFee the lateRentFee to set
	 */
	public void setLateRentFee(double lateRentFee) {
		this.lateRentFee = lateRentFee;
	}

	/**
	 * @return the lateRentFeePerDay
	 */
	public double getLateRentFeePerDay() {
		return lateRentFeePerDay;
	}

	/**
	 * @param lateRentFeePerDay the lateRentFeePerDay to set
	 */
	public void setLateRentFeePerDay(double lateRentFeePerDay) {
		this.lateRentFeePerDay = lateRentFeePerDay;
	}

	/**
	 * @return the petsAllowed
	 */
	public boolean isPetsAllowed() {
		return petsAllowed;
	}

	/**
	 * @param petsAllowed the petsAllowed to set
	 */
	public void setPetsAllowed(boolean petsAllowed) {
		this.petsAllowed = petsAllowed;
	}

	/**
	 * @return the petDepositFee
	 */
	public double getPetDepositFee() {
		return petDepositAmount;
	}

	/**
	 * @param petDepositFee the petDepositFee to set
	 */
	public void setPetDepositFee(double petDepositFee) {
		this.petDepositAmount = petDepositFee;
	}

	/**
	 * @return the petDepositRefundable
	 */
	public boolean isPetDepositRefundable() {
		return petDepositRefundable;
	}

	/**
	 * @param petDepositRefundable the petDepositRefundable to set
	 */
	public void setPetDepositRefundable(boolean petDepositRefundable) {
		this.petDepositRefundable = petDepositRefundable;
	}
	
	/**
	 * @param payment
	 */
	public void addPayment(Payment payment){
		payments.add(payment);
	}
	
	/**
	 * @param tenant
	 */
	public void addTenant(Tenant tenant){
		if(!tenants.contains(tenant)){
			tenants.add(tenant);
		}
	}
	
	/**
	 * Removes a Tenant from this Lease
	 * 
	 * @param tenant
	 * @return true if successful
	 */
	public boolean removeTenant(Tenant tenant){
		boolean success = tenants.remove(tenant);
		
		if(success){
			for(Iterator<Payment> it = payments.iterator(); it.hasNext();){
				Payment payment = it.next();
				
				payment.removeTenant(tenant);
				
				if(payment.getAssociatedTenants().size() == 0){
					it.remove();
				}
			}
		}
		
		return success;
	}
	
	/**
	 * Removes a Payment from this Lease
	 * 
	 * @param payment
	 * @return true if successful
	 */
	public boolean removePayment(Payment payment){
		return payments.remove(payment);
	}
	
	/**
	 * @return List<Payment>
	 */
	public List<Payment> getPayments(){
		return payments;
	}
	
	/**
	 * Returns the total dollar amount collected during this Lease
	 * @return double
	 */
	public double getTotalTakenIn(){
		double totalIn = 0.0;
		
		for(Payment payment : getPayments()){
			PaymentType type = payment.getType();
			
			if(type.isIncoming()){
				totalIn += payment.getAmount();
			}
		}
		
		return totalIn;
	}
	
	/**
	 * Returns the total paid out during this Lease
	 * @return double
	 */
	public double getTotalPaidOut(){
		double totalOut = 0.0;
		
		for(Payment payment : getPayments()){
			PaymentType type = payment.getType();
			
			if(!type.isIncoming()){
				totalOut += payment.getAmount();
			}
		}
		
		return totalOut;
	}
	
	/**
	 * Returns list of Payments associated with Tenant <code>tenant</code>
	 * @param tenant
	 * @return {@link ArrayList} of Tenant objects
	 */
	public ArrayList<Payment> getPaymentsForTenant(Tenant tenant){
		ArrayList<Payment> tenantPayments = new ArrayList<Payment>();
		
		for(Payment payment : payments){
			for(Tenant t : payment.getAssociatedTenants()){
				if(t.equals(tenant)){
					tenantPayments.add(payment);
				}
			}
		}
		
		return tenantPayments;
	}
	
	/**
	 * Method to generate a report between <code>startDate</code> and <code>endDate</code>
	 * @param startDate
	 * @param endDate
	 * @return String
	 */
	public String generateReport(Date startDate, Date endDate){
		if(startDate == null || endDate == null) return null;
		
		SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");
		DecimalFormat df = new DecimalFormat("0.00");
		String startDateStr = sdf.format(startDate);
		String endDateStr = sdf.format(endDate);
		
		StringBuffer strBuffer = new StringBuffer("For lease " + toString() +
				" between " + startDateStr + " and " + endDateStr + ":");
		
		double totalIn = 0.0;
		double totalOut = 0.0;
		
		for(Payment payment : getPayments()){
			PaymentType paymentType = payment.getType();
			Date thisPymtDate = payment.getDateReceived();
			
			if(!datesQualify(startDate, endDate, thisPymtDate)) continue;
			
			String sentOrReceived = null;
			if(paymentType.isIncoming()){
				sentOrReceived = " received ";
				totalIn += payment.getAmount();
			}else {
				sentOrReceived = " sent ";
				totalOut += payment.getAmount();
			}
			
			if(datesQualify(startDate, endDate, thisPymtDate)){
				String str = "\n\tPayment " + payment.getPaymentName() + " (" + paymentType + ") for $" +
						df.format(payment.getAmount()) + " " + sentOrReceived + " on " + sdf.format(payment.getDateReceived())
						+ " from ";
				
				int numAdded = 0;
				for(Tenant tenant : payment.getAssociatedTenants()){
					str += (numAdded > 0) ? ", " : "";
					str += tenant.getFirstName() + " " + tenant.getLastName() + " ";
					numAdded++;
				}
				
				strBuffer.append(str);
			}
		}
		
		strBuffer.append("\nDuring the dates of " + startDateStr + " - " + endDateStr + "\n");
		strBuffer.append("\tA total of $" + df.format(totalIn) + " was brought in\n");
		strBuffer.append("\tA total of $" + df.format(totalOut) + " was paid out\n");
		
		return strBuffer.toString();
	}
	
	/**
	 * 
	 * Tests if <code>paymentDate</code> falls on or between the given
	 * <code>startDate</code> and <code>endDate</code>.<br>
	 * <br>
	 * return true iff startDate <= paymentDate <= endDate
	 * 
	 * @param startDate
	 * @param endDate
	 * @param paymentDate
	 * @return boolean
	 */
	private boolean datesQualify(Date startDate, Date endDate, Date paymentDate){
		int startVsPayment = startDate.compareTo(paymentDate);
		
		if(startVsPayment <= 0){	// if startDate is before or equal to paymentDate
			int endVsPayment = endDate.compareTo(paymentDate);
			
			if(endVsPayment >= 0){	// if endDate is after or equal to paymentDate
				return true;
			}
		}

		return false;
	}
	
	/* (non-Javadoc)
	 * @see propertymgmt.interfaces.Reportable#generateReport()
	 */
	public String generateReport(){
		StringBuffer strBuffer = new StringBuffer("For lease " + toString() + ":");

		SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");
		DecimalFormat df = new DecimalFormat("0.00");
		
		strBuffer.append("\n\tDeposit amount: $" + df.format(getDepositAmount()));
		strBuffer.append("\n\tRent amount: $" + df.format(getRentAmount()));
		strBuffer.append("\n\tRent due on the " + getRentDueDate() +
				getNumberPostfix(getRentDueDate()) + " day of the month");
		strBuffer.append("\n\tRent late on the " + getRentLateDate() +
				getNumberPostfix(getRentLateDate()) + " day of the month");
		strBuffer.append("\n\tLate rent fee: $" + df.format(getLateRentFee()));
		strBuffer.append("\n\tLate rent fee per day: $" + df.format(getLateRentFeePerDay()));
		
		if(isPetsAllowed()){
			String refundable = ((isPetDepositRefundable()) ? " (refundable)" : " (non-refundable)");
			strBuffer.append("\n\tPets allowed with a deposit of: $" + df.format(getPetDepositFee()) +
					refundable);
		}else {
			strBuffer.append("\n\tPets were not allowed");
		}
		
		strBuffer.append("\n\n\tA total of $" + df.format(getTotalTakenIn()) + " has been received");
		for(Payment payment : getPayments()){
			PaymentType paymentType = payment.getType();
			int numAdded = 0;
			
			if(paymentType.isIncoming()){
				String str = "\n\t\t$" + df.format(payment.getAmount()) + " (" + payment.getPaymentName() + ") received " + 
						sdf.format(payment.getDateReceived()) + " from ";
				
				for(Tenant tenant : payment.getAssociatedTenants()){
					str += (numAdded > 0) ? ", " : "";
					str += tenant.getFirstName() + " " + tenant.getLastName() + " ";
					numAdded++;
				}

				strBuffer.append(str);
			}
		}
		
		strBuffer.append("\n\tA total of $" + df.format(getTotalPaidOut()) + " was paid out");

		for(Payment payment : getPayments()){
			PaymentType paymentType = payment.getType();
			
			if(!paymentType.isIncoming()){
				String str = "\n\t\t$" + df.format(payment.getAmount()) + " (" + payment.getPaymentName() + ") sent " + 
					sdf.format(payment.getDateReceived());
				
				if(payment.getAssociatedTenants().size() > 0){
					int numAdded = 0;
					str += " b/c of tenants ";
					for(Tenant tenant : payment.getAssociatedTenants()){
						str += (numAdded > 0) ? ", " : "";
						str += tenant.getFirstName() + " " + tenant.getLastName() + " ";
						numAdded++;
					}
				}
				
				strBuffer.append(str);
			}
		}
		
		return strBuffer.toString();
	}
	
	/**
	 * Returns a postfix for a given day of a month
	 * @param dayOfMonth
	 * @return String
	 */
	private String getNumberPostfix(int dayOfMonth){
		String postFix = null;
		
		if(dayOfMonth == 1 || dayOfMonth == 21 || dayOfMonth == 31){
			postFix = "st";
		}else if(dayOfMonth == 2 || dayOfMonth == 22){
			postFix = "nd";
		}else if(dayOfMonth == 3 || dayOfMonth == 23){
			postFix = "rd";
		}else {
			postFix = "th";
		}
		
		return postFix;
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	public String toString(){
		return getFormattedString();
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		long temp;
		temp = Double.doubleToLongBits(depositAmount);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		temp = Double.doubleToLongBits(lateRentFee);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		temp = Double.doubleToLongBits(lateRentFeePerDay);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		result = prime * result
				+ ((leaseEndDate == null) ? 0 : leaseEndDate.hashCode());
		result = prime * result
				+ ((leaseStartDate == null) ? 0 : leaseStartDate.hashCode());
		result = prime * result
				+ ((payments == null) ? 0 : payments.hashCode());
		temp = Double.doubleToLongBits(petDepositAmount);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		result = prime * result + (petDepositRefundable ? 1231 : 1237);
		result = prime * result + (petsAllowed ? 1231 : 1237);
		temp = Double.doubleToLongBits(rentAmount);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		result = prime * result + rentDueDate;
		result = prime * result + rentLateDate;
		result = prime * result + ((tenants == null) ? 0 : tenants.hashCode());
		return result;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Lease other = (Lease) obj;
		if (Double.doubleToLongBits(depositAmount) != Double
				.doubleToLongBits(other.depositAmount))
			return false;
		if (Double.doubleToLongBits(lateRentFee) != Double
				.doubleToLongBits(other.lateRentFee))
			return false;
		if (Double.doubleToLongBits(lateRentFeePerDay) != Double
				.doubleToLongBits(other.lateRentFeePerDay))
			return false;
		if (leaseEndDate == null) {
			if (other.leaseEndDate != null)
				return false;
		} else if (!leaseEndDate.equals(other.leaseEndDate))
			return false;
		if (leaseStartDate == null) {
			if (other.leaseStartDate != null)
				return false;
		} else if (!leaseStartDate.equals(other.leaseStartDate))
			return false;
		if (payments == null) {
			if (other.payments != null)
				return false;
		} else if (!payments.equals(other.payments))
			return false;
		if (Double.doubleToLongBits(petDepositAmount) != Double
				.doubleToLongBits(other.petDepositAmount))
			return false;
		if (petDepositRefundable != other.petDepositRefundable)
			return false;
		if (petsAllowed != other.petsAllowed)
			return false;
		if (Double.doubleToLongBits(rentAmount) != Double
				.doubleToLongBits(other.rentAmount))
			return false;
		if (rentDueDate != other.rentDueDate)
			return false;
		if (rentLateDate != other.rentLateDate)
			return false;
		if (tenants == null) {
			if (other.tenants != null)
				return false;
		} else if (!tenants.equals(other.tenants))
			return false;
		return true;
	}
}